<body></body>
<script>
  let activeEffect = null
  const effectStack = [] //effect栈
  const bucket = new WeakMap()
  const data = {foo: 1, bar: 2}

  const obj = new Proxy(data, {
    get(target, key) {
      track(target, key)
      return target[key]
    },
    set(target, key, newVal) {
      target[key] = newVal
      trigger(target, key)
    }
  })

  function track(target, key) {
    if (!activeEffect) return
    let depMap = bucket.get(target)
    if (!depMap) bucket.set(target, (depMap = new Map()))
    let deps = depMap.get(key)
    if (!deps) depMap.set(key, (deps = new Set()))

    const effectSet = deps.add(activeEffect)
    activeEffect.deps.push(effectSet)
  }

  function trigger(target, key) {
    let depMap = bucket.get(target)
    if (!depMap) return
    const depsSet = depMap.get(key)

    const effectsToRun = new Set(depsSet) || new Set()
    effectsToRun.forEach(effectFn => {
      if (effectFn?.options?.scheduler) effectFn.options.scheduler(effectFn) //执行调度
      else effectFn()
    })
  }

  function effect(fn, options) {
    // 这里的 effectFn是一个独立函数，就是每一次effect开辟一个新的空间，使用的是新的指针
    function effectFn() {
      cleanup(effectFn)
      activeEffect = effectFn

      effectStack.push(effectFn)
      const res = fn()
      effectStack.pop(effectFn)
      activeEffect = effectStack[effectStack.length - 1]
      return res
    }
    effectFn.options = options
    effectFn.deps = [] //反向收集 effectSet的引用
    if (!effectFn?.options?.lazy) effectFn()
    return effectFn
  }
  function cleanup(effectFn) {
    effectFn.deps.forEach(depSet => depSet.delete(effectFn))
    effectFn.deps.length = 0 //遍历所有的 Set集合删除同一个副作用
  }

  const jobQueue = new Set() // 任务队列控制逻辑 // 任务队列（自动去重）
  let isFlushing = false // 队列刷新状态
  const p = Promise.resolve() // 微任务触发器

  function flushJob() {
    if (isFlushing) return
    isFlushing = true
    p.then(() => jobQueue.forEach(job => job())).finally(() => (isFlushing = false))
  }

  function traverse(value, seen = new Set()) {
    if (typeof value !== 'object' || value === null || seen.has(value)) return
    seen.add(value)
    for (const k in value) {
      traverse(value[k], seen)
    }
    return value
  }
  // watch 函数接收 3个参数，source 是响应式数据，cb是回调函数,options配置
  function watch(source, cb, options = {}) {
    let getter
    if (typeof source === 'function') getter = source
    else getter = () => traverse(source)
    let oldValue, newValue

    let cleanup //误区：这里的 oninvalidata不会watch直接调用，是开发者在watch使用过程去调用
    const oninvalidate = fn => {
      cleanup = fn
    }
    const job = () => {
      newValue = effectFn()
      if (cleanup) cleanup() //在回调函数cd之前，先调用注册过期回调
      cb(newValue, oldValue, oninvalidate) //将 onInvalidate 作为回调函数的第三个参数，以便用户使用
      oldValue = newValue
    }
    const effectFn = effect(() => getter(), {
      lazy: true,
      scheduler: job
    })
    if (options.immediate) job()
    else oldValue = effectFn()
  }
  /*认真看：在这段代码中，我们首先定义了cleanup变量，这个变量用来存储用户通过onInvalidate 函数注册的过期回调。
可以看到onInvalidate 函数的实现非常简单，只是把过期回调赋值给了cleanup 变量。这里的关键点在 job函数内，
每次执行回调函数cb之前，先检查是否存在过期回调，如果存在，则执行过期回调函数cleanup。
最后我们把 onInvalidate 函数作为回调函数的第三个参数传递给 cb，以便用户使用。
*/
  let finalData
  watch(obj, async (newVlaue, oldValue, oninvalidate) => {
    let expired = false //  1、定义一个标志，代表当前副作用 函数是否过期，默认为 false，代表没有过期
    oninvalidate(() => (expired = true)) // 2、 当过期时，将 expired 设置为 true

    const res = await fetch('http://localhost:3000/list')

    if (!expired) {
      // 3、只有当前副作用函数的执行没有过期时，才会执行后续操作
      finalData = await res.json()
      console.log(finalData)
    }
  })
  // 注意闭包：watch回调里面，expired是一个区部变量，执行一次创建一次，被引用到会保存指针指向
  // 而 watch的实现里面，刚好就是将 这个引用改成 ture，所以每一次 !expired都没办法执行
  // 直到 最后一个触发，expired 在watch的实现回调里面，继续触发 把上一个 expired设置true

  // 场景：
  // 当时 同时进去 面试的了 一批人，只有最后一个默认通过了 ，
  // 同一时间面试太多人，面试官累了，每次都是把上一个人叫出去（把上一个expired引用改成true），
  // 最后一个是没有被喊出去的，所以 最后的那个人通过了面试（expired初始是false，这个!expired 成立了），

  // watch使用者角度，watch目前传递了两个参数，一个obj，一个执行函数，
  // 每一次的调用都是 新的expried，就是false，这个expried的引用在oninvalidate回调传过去
  obj.foo++
  setTimeout(() => obj.foo++, 200) //两百毫秒后触发watch
</script>
